package framework.server;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.rmi.AccessException;
import java.rmi.NoSuchObjectException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.apache.log4j.Logger;

import common.Configuration;
import common.LogCategory;

import sun.misc.Signal;
import sun.misc.SignalHandler;

import framework.Message;
import framework.Trait;
import framework.Service;
import framework.server.cfg.ServiceCfg;
import framework.server.cfg.ServicesCfgHandler;
import framework.service.cluster.Cluster;
import framework.service.cluster.ForwardMessage;
import framework.utils.ArrayMap;

/**
 * The main class of the framework.<br>
 * it is a container for services
 *
 * @author	Orlin Tomov
 * @version	0.2
 *
 */

public class Server implements Runnable, Trait {

	/** The admin registry port */
	final static int REGISTRY_PORT = Registry.REGISTRY_PORT;
	/** The admin registry name */
	final static String REGISTRY_NAME = "admin";

	private static boolean                running;
	/** The config files and the last modified date for each of them*/
	private final Map<ServicesCfgHandler, Long> config;
	private final List<Service>           container;
	private Loader                        loader = new Loader();
	private final Map<String, Mail>       queue;
	private Registry                      registry;
	private final Date                    started = new Date();

	public final static String version = "Server Version 0.2";

	private static final Logger log = LogCategory.getCategory(Server.class);

	/**
	 * SingletonHolder is loaded on the first execution of
	 * Singleton.getInstance() or the first access to SingletonHolder.INSTANCE,
	 * not before.
	 */
	private static class SingletonHolder {
		public static final Server INSTANCE = new Server();
	}

	/**
	 * Starts a local server.
	 * @return the server instance
	 *
	 */
	public static Server getServer() {
		Server server = SingletonHolder.INSTANCE;
		synchronized (server) {
			if (server.registry == null) {
				File local = new File (Configuration.formDefaultConfigurationPath("local_services.xml"));
				if (local.exists()) {
					getServer(local);
				}
				server.init(REGISTRY_PORT);
				Thread thread = new Thread(server);
				thread.setName(version);
				thread.start();
			}
		}
		return server;
	}

	/**
	 * Gets server instance with a config file
	 * @param configFile the config file
	 * @return the server instance
	 *
	 */
	public static Server getServer(File configFile) {
		Server server = SingletonHolder.INSTANCE;
		try {
			server.prepare(configFile);
		} catch (FileNotFoundException exception) {
			/**/Server.log.error("Can't set up services", exception);
		}
		return server;
	}

	/**
	 * Gets server instance with a bunch of config files
	 * @param configFiles the bunch of config files
	 * @return the server instance
	 *
	 */
	public static Server getServer(Collection<File> configFiles) {
		Server server = SingletonHolder.INSTANCE;
		if (configFiles != null) {
			for (File configFile: configFiles) {
				try {
					server.prepare(configFile);
				} catch (FileNotFoundException exception) {
					/**/Server.log.error("Can't set up services", exception);
				}
			}
		}
		return server;
	}

	/**
	 * Construct the server
	 *
	 */
	private Server() {
		running = false;
		/** hash is file based. @see AbstractCfgHandler#hashCode() 
		 * @return */
		config = new HashMap<ServicesCfgHandler, Long>();
		container = new ArrayList<Service>();
		queue = new ArrayMap<String, Mail>();
	}

	/** connects the admin. 
	 * @param registryPort */
	protected synchronized void init(int registryPort) {
		try {
			Registry registry = LocateRegistry.createRegistry(registryPort);
			AdminImpl admin = new AdminImpl(this);
			registry.bind(REGISTRY_NAME, admin);
			this.registry = registry;
			log.info(REGISTRY_NAME + ":" + registryPort);
		} catch(Exception exception) {
			log.warn("Can't set up server admin: " + exception.getMessage());
			//System.exit(1);
		}
	}

	/**
	 * Returns running status
	 *
	 * @return	running status of the server
	 * @see	#start()
	 * @see	#stop()
	 */
	@Override
	public boolean isRunning() {
		return running;
	}

	/**
	 * Starts the server
	 *
	 * @see	#isRunning()
	 * @see	#stop()
	 */
	void start() {
		Runtime.getRuntime().addShutdownHook(
				new Thread() {
					public void run() {
						setName("shutdownhook");
						log.info("Server is shutdown.");
						running = false;
					}
				}
		);

		SignalHandler handler = new SignalHandler () {
			public void handle(Signal sig) {
				if (running) {
					running = false;
					log.debug("Signal " + sig);
					log.info("Shutting down server ...");
				} else {
					/** only on the second attempt do we exit */
					log.info("Server shutdown interrupted!");
					System.exit(0);
				}
			}
		};
		Signal.handle(new Signal("INT"), handler);
		Signal.handle(new Signal("TERM"), handler);

		running = true;
		Server.log.info("Server is running ");
		try {
			Iterator<ServicesCfgHandler> iterator = config.keySet().iterator();
			while (iterator.hasNext()) {
				ServicesCfgHandler cfg = iterator.next();
				update(cfg);
			}
		} catch (FileNotFoundException exception) {
			/**/Server.log.error("Can't set up services", exception);
		}
		//run();
	}

	/**
	 * Stops the server
	 *
	 * @see	#isRunning()
	 * @see	#start()
	 */
	void stop() {
		running = false;
		Server.log.info("Server is stopping ");
	}

	/**
	 * Main method of the thread
	 *
	 * @see	#isRunning()
	 * @see	#start()
	 * @see	#stop()
	 */
	@Override
	public void run() {
		start();
		while (running) {
			try {
				Thread.sleep(100L);
			} catch (InterruptedException exception) {
				Server.log.warn(exception.getMessage());
			}
			Iterator<ServicesCfgHandler> iterator = config.keySet().iterator();
			while (iterator.hasNext()) {
				ServicesCfgHandler cfg = iterator.next();
				File configFile = cfg.getFile();
				long lastModified = config.get(cfg).longValue();
				if (configFile.lastModified() > lastModified) {
					try {
						update(cfg);
					} catch (FileNotFoundException exception) {
						/**/Server.log.error("Can't set up services", exception);
					}
				}
			}
			if (checkAndUpdate()) {
				try {
					iterator = config.keySet().iterator();
					while (iterator.hasNext()) {
						update(iterator.next());
					}
				} catch (FileNotFoundException exception) {
					/**/Server.log.error("Can't set up services", exception);
				}
			} else {
				cleanQueue();
				//logMsg();
			}
		}

		Server.log.info("Stopping services");
		System.out.println("Stopping services");
		synchronized (container) {
			for (Service service : container) {
				service.stoping();
			}
		}
		/**/try {
		/**/	Thread.sleep(100L);
		/**/} catch (InterruptedException exception) {
		/**/	Server.log.warn(exception.getMessage());
		/**/}
		Server.log.info("unbind " + REGISTRY_NAME);
		System.out.println("unbind " + REGISTRY_NAME);
		try {
			registry.unbind(REGISTRY_NAME);
			UnicastRemoteObject.unexportObject(registry, true);
		} catch (NoSuchObjectException e) {
		} catch (AccessException e) {
		} catch (RemoteException e) {
		} catch (NotBoundException e) {
		}

		Server.log.info("Services has stopped");
		System.out.println("Services has stopped");
		/**/System.exit(0);
	}

	/*
	 * logs count of messages
	 *
	private void logMsg() {
		Map<Integer, Integer> map = new TreeMap<Integer, Integer>();
		synchronized (started) /**queue / {
			Iterator<Entry<String, Message>> i  = queue.entrySet().iterator();
			while (i.hasNext()) {
				Entry<String, Message> entry = i.next();
				Message message = entry.getValue();
				Integer count = map.get(message.getMessage());
				if (count == null) {
					count = new Integer(0);
				}
				count++;
				map.put(message.getMessage(), count);
			}
		}
		/*try {
			FileWriter out = new FileWriter("../log/queue.log", true);
			out.append((new Date()).toString());
			Iterator<Entry<Integer, Integer>> i  = map.entrySet().iterator();
			while (i.hasNext()) {
				Entry<Integer, Integer> entry = i.next();
				Integer message = entry.getKey();
				Integer count = entry.getValue();
				out.append(", " + message + ":" + count);
			}
			out.append("\n");
			out.flush();
			out.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}/
	}/**/

	/**
	 * Cleans a message from the queue without recipient
	 *
	 */
	private void cleanQueue() {
		synchronized (started) /**queue*/ {
			if (queue.entrySet().iterator().hasNext()) {
				Entry<String, Mail> entry = queue.entrySet().iterator().next();
				String key = entry.getKey();
				if (getService(key) == null) {
					Mail mail = entry.getValue();
					Server.log.debug("remove message: " + mail.getMessageName() + " from " + mail.getSender() + " to " + key );
					queue.remove(key);
					// TODO
					if (getService(Cluster.class.getName()) != null) {
						sendMessage(new ForwardMessage(entry), "Server", Cluster.class.getName());
					}
				}
			}
		}
	}

	/**
	 * Pushes the message to the queue
	 *
	 * @param	message	the message
	 * @param	sender	message sender
	 * @param	receiver	message recipient
	 * @see	framework.Trait#sendMessage(framework.Message, java.lang.String, java.lang.String)
	 */
	public void sendMessage(Message message, String sender, String receiver) {
		Mail mail = new Mail(sender, message);
		sendMail(mail, receiver);
	}

	/**
	 * Pushes the message to the queue with delay
	 *
	 * @param	message	the message
	 * @param	sender	message sender
	 * @param	receiver	message recipient
	 * @param 	timeout	delay (in ms)
	 * @see	framework.Trait#sendMessage(framework.Message, java.lang.String, java.lang.String, int)
	 */
	public void sendMessage(Message message, String sender, String receiver, int timeout) {
		Mail mail = new Mail(sender, message, timeout);
		sendMail(mail, receiver);
	}

	/**
	 * Pushes the mail to the queue
	 *
	 * @param	mail	the mail
	 * @param	receiver	message recipient
	 */
	public void sendMail(Mail mail, String receiver) {
		synchronized (started) /**queue*/ {
			if (mail.getScheduled() != null) {
				/** check for same scheduled message already in queue */
				//System.out.println("new scheduled message: " + msgEnvelope.getMessageName() + " from " + msgEnvelope.getSender() + " to " + serviceName + " with " + ((Msg)message).getObject());
				for(Entry<String, Mail> entry: queue.entrySet()) {
					Mail checkMsg = entry.getValue();
					if (checkMsg.getScheduled() == null) {
						continue;
					}
					//System.out.println("old scheduled message: " + checkMsg.getMessageName() + " from " + checkMsg.getSender() + " to " + key + " with " + checkMsg.getObject());
					if (checkMsg.compareTo(mail) == 0) {
						//System.out.println("messages are same.");
						//queue.remove(key);
						//break;
						entry.setValue(mail);
						return;
					}
				}
			}

			queue.put(receiver, mail);
		}
	}

	/**
	 * Pulls the next unscheduled message from the queue. Dispatcher method.
	 *
	 * @return	next message from the queue and service for
	 */
	public Entry<String, Mail> pullMailEntry() {
		Entry<String, Mail> entry = null;
		synchronized (started) /**queue*/ {
			Iterator<Entry<String, Mail>> iterator = queue.entrySet().iterator();
			while (iterator.hasNext()) {
				entry = iterator.next();
				Mail mail = entry.getValue();
				if (mail.getScheduled() == null) {
					//Server.log.debug("pull message: " + mail.getMessageName() + " from " + mail.getSender() + " to " + entry.getKey() );
					iterator.remove();
					break;
				}
				entry = null;
			}
		}
		return entry;
	}

	/**
	 * Pulls the next message from the queue for specific service by name
	 *
	 * @param	serviceName	the service what message is looking for
	 * @return	message next message from the queue
	 * @see	framework.Trait#pullMessage(java.lang.String)
	 */
	@Override
	public <M extends Message> M pullMessage(String serviceName) {
		Mail mail = pullMail(serviceName);
		M message = null;
		if (mail != null) {
			message = mail.getMessage();
		}
		return message;
	}

	/**
	 * Pulls the next message from the queue for specific service by name. MessageService method
	 *
	 * @param	serviceName	the service what message is looking for
	 * @return	message next message from the queue
	 */
	public Mail pullMail(String serviceName) {
		Mail mail = null;
		synchronized (started) /**queue*/ {
			mail = queue.remove(serviceName);
			if (mail != null) {
				if (mail.getScheduled() != null) {
					Date now = new Date();
					if (now.compareTo(mail.getScheduled()) < 0) {
						sendMail(mail, serviceName);
						//System.out.println(msgEnvelope.sheduled.getTime() - now.getTime());
						mail = null;
					}
				}
			}
			//if (message != null) {
			//Server.log.debug("pull message: " + mail.getMessageName() + " from " + mail.getSender() + " to " + serviceName );
			//}
		}
		return mail;
	}

	/**
	 * Gets queue size. Dispatcher method
	 * 
	 * @return	queue size
	 */
	public int queueSize() {
		synchronized (started) /**queue*/ {
			return queue.size();
		}
	}

	/**
	 * Interpreters the server commands
	 *
	 * @param	command text
	 */
	public String command(String command) {
		/**/Server.log.debug("command "+command);
		String result = version + "\n";
		result += "runnig since: " + started + "\n";
		if (command.equalsIgnoreCase("help")) {
			result += "help\n"+
			          "shutdown\n"+
			          "queue\n"+
			          "status\n"+
			          "start service\n"+
			          "stop service\n"+
			          "info service\n";
			return result;
		}
		if (command.equalsIgnoreCase("queue")) {
			result += getQueue();
			return result;
		}
		synchronized (container) {
			if (command.equalsIgnoreCase("shutdown")) {
				stop();
				result += "ok.";
				return result;
			}
			if (command.equalsIgnoreCase("status")) {
				result += getServiceNames(false);
				return result;
			}
			if (command.equalsIgnoreCase("trace")) {
				result += getServiceNames(true);
				return result;
			}
			if (command.startsWith("stop ")) {
				result += stopService(command.substring(5));
				return result;
			}
			if (command.startsWith("start ")) {
				result += startService(command.substring(6));
				return result;
			}
			if (command.startsWith("info ")) {
				result += infoService(command.substring(5));
				return result;
			}
		}
		result += "unknown command!";
		return result;
	}

	/**
	 * Returns the list of deployed services
	 *
	 * @return	the list of deployed services
	 * @see	#getService(String)
	 */
	private String getQueue() {
		StringBuffer result = new StringBuffer();
		synchronized (started) /**queue*/ {
			for (Entry<String, Mail> entry: queue.entrySet()) {
				String serviceName = entry.getKey();
				Mail mail = entry.getValue();
				result.append("message " + mail.getMessageName() + " from " + mail.getSender() + " to " + serviceName);
				result.append(" obj:" + mail.getMessage());
				if (mail.getScheduled() != null) {
					result.append(" sheduled to " + mail.getScheduled());
				}
				result.append("\n");
			}
		}
		if (result.length() == 0) {
			return "Queue is empty.";
		}
		return result.toString();
	}

	/**
	 * Returns the list of deployed services
	 *
	 * @return	the list of deployed services
	 * @see	#getService(String)
	 */
	private String getServiceNames(boolean trace) {
		synchronized (container) {
			String names = "";
			if (trace) {
				Map<Thread, StackTraceElement[]> traces = Thread.getAllStackTraces();
				for (Thread thread: traces.keySet()) {
					if (thread.getName().equals("main")) {
						names = getTrace((Thread) thread, names);
						//names += thread.getName() + "\n";
					}
				}
			}
			List<String> list = new ArrayList<String>();
			for (Service service: container) {
				if (((Thread)service).isAlive()) {
					if (trace) {
						names += service.getServiceName() + " - " + service.getClass().getName() + "\n";
						names = getTrace((Thread) service, names);
					} else {
						list.add(service.getServiceName());
					}
				}
			}
			if (!trace) {
				names = list.toString();
			}
			return names;
		}
	}

	private String getTrace(Thread thread, String text) {
		StackTraceElement[] elements = thread.getStackTrace();
		for (int i = 0; i < elements.length; i++) {
			text += elements[i].toString() + "\n";
		}
		text += "\n";
		return text;
	}

	private String startService(String name) {
		Service service = getService(name);
		if (service != null) {
			service.starting();
			return "ok.";
		}
		return "unknown service!";
	}

	private String stopService(String name) {
		Service service = getService(name);
		if (service != null) {
			service.stoping();
			return "ok.";
		}
		return "unknown service!";
	}

	private String infoService(String name) {
		Service service = getService(name);
		if (service != null) {
			String result = service.info();
			return result;
		}
		return "unknown service!";
	}

	/**
	 * Returns the specified service by name or class name.
	 * If there is not a service deployed with this name returns null. Dispatcher method.
	 *
	 * @param	name	a service name
	 * @return	specified service
	 */
	@SuppressWarnings("unchecked")
	public <S extends Service> S getService(String name) {
		synchronized (container) {
			for (Service service: container) {
				if (service.getServiceName().equalsIgnoreCase(name)) {
					return (S) service;
				}
				if (service.getClass().getName().equalsIgnoreCase(name)) {
					return (S) service;
				}
			}
		}
		return null;
	}

	/**
	 * Returns the specified services.
	 * If there is not services deployed with this name returns empty list. ClientManager method.
	 *
	 * @param	name	a service name
	 * @return	specified service
	 */
	public List<? extends Service> getServices(String name) {
		List<Service> services = new ArrayList<Service>();
		synchronized (container) {
			for (Service service: container) {
				if (service.getServiceName().equalsIgnoreCase(name)) {
					services.add(service);
				} else
					if (service.getClass().getName().equalsIgnoreCase(name)) {
						services.add(service);
					}
			}
		}
		return services;
	}

	/**
	 * Returns the class loader of the server.
	 *
	 * @return	the class loader
	 */
	@Override
	public ClassLoader getLoader() {
		return loader;
	}

	/**
	 * Check all jar files if they are modified recently and update them
	 *
	 * @return	boolean value if the jar files are updated or not
	 */
	private boolean checkAndUpdate() {
		boolean isUpdated = false;
		String path;
		String pathName = loader.getPathName();
		if (pathName == null) {
			path = Configuration.formDefaultPath("", "");
		} else {
			path = Configuration.formDefaultPath(pathName, "");
		}
		Map<String, Long> jarNames = loader.getJarNames();
		for (String jarName: jarNames.keySet()) {
			File jarFile = new File(path + jarName);
			long lastModified = jarNames.get(jarName).longValue();
			if (jarFile.lastModified() > lastModified) {
				updateJarFile(jarFile);
				isUpdated = true;
			}
		}
		return isUpdated;
	}

	/**
	 * Update a jar file i.e. remove all classes possessed by the jar file from the classes hashtable
	 * 
	 * @param	jar	file File
	 * 
	 */
	private void updateJarFile(File jarFile) {
		try {
			synchronized (container) {
				//System.out.println("Stopping services");
				for (Service service: container) {
					service.stoping();
				}

				container.clear();

				ZipFile zip = new ZipFile(jarFile);
				Enumeration<?> entries = zip.entries();
				while(entries.hasMoreElements()) {
					ZipEntry entry = (ZipEntry)entries.nextElement();
					Map<String, Class> classes = loader.getClasses();
					Iterator<String> iterator = classes.keySet().iterator();
					while(iterator.hasNext()) {
						if (entry.getName().equals(iterator.next())) {
							iterator.remove();
						}
					}
				}
			}
		} catch (IOException exception) {
			/**/Server.log.error("Could not read file: " + jarFile, exception);
		}
	}

	private void prepare(File configFile) throws FileNotFoundException {
		ServicesCfgHandler servicesCfgHandler = new ServicesCfgHandler();
		servicesCfgHandler.parseURI(configFile);
		for (ServiceCfg serviceCfg: servicesCfgHandler.getServiceCfgs()) {
			loader.register(serviceCfg.getPath(), serviceCfg.getJar());
		}
		config.put(servicesCfgHandler, new Long(configFile.lastModified()));
	}

	private void update(ServicesCfgHandler servicesCfgHandler) throws FileNotFoundException {
		File configFile = servicesCfgHandler.getFile();
		servicesCfgHandler.parseURI(configFile);

		synchronized (container) {
			//System.out.println("Stopping services");
			for (ServiceCfg serviceCfg: servicesCfgHandler.getServiceCfgs()) {
				Service service = getService(serviceCfg.getName());
				if (service != null) {
					service.stoping();
					container.remove(service);
				}
			}
			for (ServiceCfg serviceCfg: servicesCfgHandler.getServiceCfgs()) {

				Service service = loader.getService(this, serviceCfg);
				container.add(service);
			}
		}

		config.put(servicesCfgHandler, new Long(configFile.lastModified()));
		//System.out.println("connection date:" + configFile.lastModified());
	}

	/**
	 * @param	service
	 */
	@Override
	public void addService(Service service) {
		synchronized (container) {
			container.add(service);
		}
	}

	/**
	 * @param	service
	 */
	@Override
	public void remove(Service service) {
		synchronized (container) {
			container.remove(service);
		}
	}
}
